package Connections;

import Gear.Factory;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.FileSystems;
import java.util.HashMap;

/**
 * Principal server class which implements all crucial methods.
 *
 * @author Rodrigo Marins Racanicci
 */
public class Server {

    /**
     * Server socket.
     */
    protected ServerSocket serverSocket;

    /**
     * Indicates that the server is alive.
     */
    protected boolean isAlive;

    /**
     * Server name;
     */
    protected String name;

    /**
     * Client socket mapping its handler.
     */
    protected HashMap<Socket, ConnectionHandler> clientSocketMap;

    /**
     * Client socket mapping its input stream.
     */
    private HashMap<Socket, InputStream> inputMap;

    /**
     * Client socket mapping its output stream.
     */
    private HashMap<Socket, OutputStream> outputMap;

    /**
     * Client socket mapping its input stream reader.
     */
    private HashMap<Socket, InputStreamReader> inputStreamReaderMap;

    /**
     * Client socket mapping its output stream reader.
     */
    private HashMap<Socket, OutputStreamWriter> outputStreamWriterMap;

    /**
     * Client socket mapping its buffered reader.
     */
    private HashMap<Socket, BufferedReader> inputBufferedReaderMap;

    /**
     * Client socket mapping its buffered writer.
     */
    private HashMap<Socket, BufferedWriter> outputBufferedWriterMap;

    /**
     * Server constructor.
     *
     * @param name Server name.
     */
    public Server(String name) {
        try {
            this.name = name;
            isAlive = true;
            serverSocket = new ServerSocket(Factory.getInstance().getSettings().getServerPort());
            clientSocketMap = new HashMap<>();
            inputMap = new HashMap<>();
            outputMap = new HashMap<>();
            inputStreamReaderMap = new HashMap<>();
            outputStreamWriterMap = new HashMap<>();
            inputBufferedReaderMap = new HashMap<>();
            outputBufferedWriterMap = new HashMap<>();

            System.out.println(name + ": created");
        } catch (IOException ex) {
            System.out.println(name + ": error creating socket");
            this.isAlive = false;
        }
    }

    /**
     * Register a client on server.
     *
     * @param client  Client socket.
     * @param handler Client handler.
     */
    protected void registerClient(Socket client, ConnectionHandler handler) {
        try {
            clientSocketMap.put(client, handler);
            inputMap.put(client, client.getInputStream());
            outputMap.put(client, client.getOutputStream());
            inputStreamReaderMap.put(client, new InputStreamReader(inputMap.get(client)));
            outputStreamWriterMap.put(client, new OutputStreamWriter(outputMap.get(client)));
            inputBufferedReaderMap.put(client, new BufferedReader(inputStreamReaderMap.get(client)));
            outputBufferedWriterMap.put(client, new BufferedWriter(outputStreamWriterMap.get(client)));

        } catch (IOException ex) {
            System.out.println(name + ": error registering client at " + client.getInetAddress());
        }

    }

    /**
     * Close client connection.
     *
     * @param client Client socket.
     */
    public void closeClient(Socket client) {
        try {
            clientSocketMap.remove(client);
            inputMap.remove(client).close();
            outputMap.remove(client).close();
            inputStreamReaderMap.remove(client).close();
            outputStreamWriterMap.remove(client).close();
            inputBufferedReaderMap.remove(client).close();
            outputBufferedWriterMap.remove(client).close();

            client.close();
        } catch (IOException ex) {
            System.out.println(name + ": Error closing client " + client.getInetAddress());
        }
    }

    /**
     * Send a command to a client.
     *
     * @param client  Client socket.
     * @param command Comannd.
     */
    public void sendCommand(Socket client, String command) {
        try {
            outputBufferedWriterMap.get(client).write(command + "\n");
            outputBufferedWriterMap.get(client).flush();

            //System.out.println("CLIENT: command " + command + " sent");

        } catch (IOException ex) {
            System.out.println("CLIENT: error sending command");
        }
    }

    /**
     * Receive a command of a client.
     *
     * @param client Client socket.
     *
     * @return String of the command
     */
    public String receiveCommand(Socket client) {
        try {
            String command = inputBufferedReaderMap.get(client).readLine();

            //System.out.println("CLIENT: command " + command + " received");
            return command;
        } catch (IOException ex) {
            System.out.println("CLIENT: error receiving command");
            return null;
        }
    }

    /**
     * Send a file to the client.
     *
     * @param client Client socket.
     * @param source File to send.
     */
    public void sendFile(Socket client, File source) {

        int bytesRead;
        int bufferSize;
        byte[] buffer;
        FileInputStream inputStream;
        OutputStream output;

        try {
            inputStream = new FileInputStream(source);
            output = outputMap.get(client);

            // Buffer de envio
            bufferSize = Factory.getInstance().getSettings().getBufferSize();
            buffer = new byte[bufferSize];

            //System.out.println(name + ": Sending " + source);

            // Envio bufferizado
            while ((bytesRead = inputStream.read(buffer, 0, bufferSize)) != -1) {
                output.write(buffer, 0, bytesRead);
                output.flush();
            }

            inputStream.close();

            this.closeClient(client);
            //System.out.println(name + ": Sent " + source + " and connection with client at " + client.getInetAddress() + " terminated");

        } catch (FileNotFoundException ex) {
            System.out.println(name + ": Invalid file " + source);
        } catch (IOException ex) {
            System.out.println(name + ": Error on socket manipulation" + source);
        }
    }

    /**
     * Receive a file from the client.
     *
     * @param client Client socket.
     *
     * @return File received.
     */
    public File receiveFile(Socket client) {

        int bytesRead;
        int bufferSize;
        byte[] buffer;
        FileOutputStream output;
        String sourceName;
        File target;
        InputStream input;
        BufferedReader bufferedReader;

        try {
            input = inputMap.get(client);
            bufferedReader = inputBufferedReaderMap.get(client);
            sourceName = bufferedReader.readLine();
            target = FileSystems.getDefault().getPath(Factory.getInstance().getSettings().getGearPath().toString(), sourceName).toFile();
            output = new FileOutputStream(target);

            //System.out.println(name + ": Receiving " + target);

            bufferSize = Factory.getInstance().getSettings().getBufferSize();
            buffer = new byte[bufferSize];

            while ((bytesRead = input.read(buffer, 0, bufferSize)) != -1) {
                output.write(buffer, 0, bytesRead);
            }

            output.flush();
            output.close();
            this.closeClient(client);
            //System.out.println(name + ": Received " + target + " and connection with client at " + client.getInetAddress() + " terminated");

            return target;

        } catch (FileNotFoundException ex) {
            System.out.println(name + ": Invalid file");
            return null;
        } catch (IOException ex) {
            System.out.println(name + ": Error on receiving file");
            return null;
        }
    }
}
